package projorientado.anpr.impl;

import projorientado.anpr.*;
import java.awt.image.BufferedImage;
import java.util.Vector;
import projorientado.image.FullSegmentation;
import projorientado.image.ImageLayerData;
import projorientado.image.Segment;

/**
 * Classe de reconhecimento de placas.<BR>
 * Esse sistema é baseado no reconhecimento de placa por blocos. Onde os tamanhos entre os caracteres da placa
 * são definidos, e que pode haver espaços maiores entre caracteres da placa, gerando blocos.
 */
public class BPlateRecognizer implements PlateRecognizer {

    private OCRPlate ocr;
    private BPlateRecognizerProps properties;
    
    public static final int
                VISIT_SEGMENTATION_INIT_NORMAL = 1,
                VISIT_SEGMENTATION_INIT_NEGATIVE = 2,
                VISIT_SEGMENTATION_COMPLETE_OK = 3,
                VISIT_SEGMENTATION_COMPLETE_ERROR = 4,
                VISIT_SEGMENTATION_FILTER = 5,
                VISIT_SEGMENTATION_NUM_SEGMENTS = 6,
                VISIT_VERTICAL_FILTER_TOTAL_NUM_LINES = 8,
                VISIT_VERTICAL_FILTER_NUM_LINES = 9,
                VISIT_LINE_PROCESS_INIT = 10,
                VISIT_LINE_SEGMENTS_COUNT = 11,
                VISIT_LINE_PROCESS_COMPLETE_INSUFFICIENT_SEGMENTS = 12,
                VISIT_LINE_PLATE_FOUND = 13,
                VISIT_LINE_PLATE_NOT_FOUND = 14,
                VISIT_LINE_PROCESS_COMPLETE = 15,
                VISIT_SEGMENTATION_ADPTIVETHRESHOLDING = 16;
    
    public BPlateRecognizer(OCRPlate ocr, BPlateRecognizerProps props) {
        this.ocr = ocr;
        properties = props;
        props.compileInformations();
    }
    
    public PlateRecognitionResult recognizePlate(BufferedImage img) {
        return recognizePlate(img,null);
    }
    
    public PlateRecognitionResult recognizePlate(BufferedImage img, PlateRecognizerVisitor visitor) {
        PlateRecognitionResult ret = new PlateRecognitionResult();
        ret.initialize();// inicia contagem de tempo
        
        recognizePlateSegments(img,ret,false,visitor); // tenta reconhecer a placa
        if(ret.getCode() == PlateRecognitionResult.PLATE_NOT_FOUND)
            recognizePlateSegments(img,ret,true,visitor); // se não tem placa, tenta reconhecer no negativo
        
        recognizeCharacters(ret,visitor); // reconheciemtno de caracteres
        ret.complete();// finaliza contagem de tempo
        return ret;
    }
    
    /**
     * Faz o reconhecimento dos caracteres das placas do resultado.<BR>
     * Basicamente esse método usa o OCRPlate para fazer o reconhecimento.
     * @param result objeto de resultado
     * @param visitor visitante
     */
    private void recognizeCharacters(PlateRecognitionResult result, PlateRecognizerVisitor visitor) {
        if(result.getCode() == PlateRecognitionResult.PLATE_RECOGNIZED) {
            for(int i = 0; i < result.getPlates().length; i++) {
                ocr.recognizePlate(result.getPlates()[i], i, visitor);
            }
        }
    }
    
    /**
     * Reconhece os segmentos de uma placa dada uma entrada de imagem.
     * @param img imagem de entrada
     * @param result objeto de resultado
     * @param negative define se é para usar imagem em negativo
     * @param visitor visitante
     */
    private void recognizePlateSegments(BufferedImage img, PlateRecognitionResult result, 
                                    boolean negative, PlateRecognizerVisitor visitor) {
        
        Vector<Segment> segments = filtredSegmentation(img,negative,visitor); // segmentação
        if(segments == null) {
            result.setCode(PlateRecognitionResult.PLATE_SEGMENTATION_ERROR); // erro
        } else {
            recognizePlateFromSegments(segments,result,visitor); // reconhecimento
        }
    }
    
    /**
     * Faz a segmentação da imagem de entrada, e filtra os segmentos gerados por tamanhos definidos nas propriedades.
     * @param img imagem
     * @param negative define se é para usar a imagem negativa
     * @param visitor visitante
     * @return segmentos da imagem filtrados por tamanho
     */
    private Vector<Segment> filtredSegmentation(BufferedImage img, boolean negative, PlateRecognizerVisitor visitor) {
        
        if(visitor != null) {
            visitor.visitLog("Iniciado processo de segmentação", 
                    negative ? VISIT_SEGMENTATION_INIT_NEGATIVE : VISIT_SEGMENTATION_INIT_NORMAL);
        }
        
        ImageLayerData pb = ImageLayerData.extractBrightnessLayer(img);
        
        if(properties.normalizeThresholding >= 0)
            pb.normalizeThresholding(properties.normalizeThresholding);
        
        if(properties.improveSin >= 0)
            pb.improveSen(properties.improveSin);
        
        if(properties.improveLinearPoint != -1)
            pb.improveLin(properties.improveLinearPoint, properties.improveLinearDiff);

        if(visitor != null) {
            visitor.visitImage(pb, "Extração de brilho e normalização", VISIT_SEGMENTATION_ADPTIVETHRESHOLDING);
        }
        
        
        
        pb = pb.adaptiveThresholding(properties.adptiveThresholdingRadius);
        
        
        
        if(negative)
            pb.negative();
        
        if(visitor != null) {
            visitor.visitImage(pb, "Processamento de adptiveThresholding", VISIT_SEGMENTATION_ADPTIVETHRESHOLDING);
        }
        
        Vector<Segment> segments = FullSegmentation.consumeSegments(pb, 0.5f, 
                properties.plateCharWidthMin, properties.plateCharWidthMax, 
                properties.plateCharHeightMin, properties.plateCharHeightMax);
        
        if(visitor != null) {
            visitor.visitValue(segments.size(), "Numero de segmentos", VISIT_SEGMENTATION_NUM_SEGMENTS);
            visitor.visitLog("Processo de segmentação finalizado", VISIT_SEGMENTATION_COMPLETE_OK);
            for(int i = 0; i < segments.size(); i++) {
                Segment s = segments.elementAt(i);
                s.copyInOriginalPosition(pb);
            }
            visitor.visitImage(pb, "Segmentos restantes", VISIT_SEGMENTATION_FILTER);
        }
        
        return segments;
    }
    
    /**
     * Faz o reconhecimento das placas presentes nos segmentos da imagem.
     * @param segments segmentos da imagem
     * @param result objeto de resultado
     * @param visitor visitante
     */
    private void recognizePlateFromSegments(Vector<Segment> segments,PlateRecognitionResult result,PlateRecognizerVisitor visitor) {
        Vector<Plate> plates = new Vector();
        
        // filtro de linhas
        Vector<Vector<Segment>> v_segments = groupSegmentsByLine(segments,properties.plateVertMaxDiff);
        if(visitor != null) {
            visitor.visitValue(v_segments.size(), "Numero total de linhas", VISIT_VERTICAL_FILTER_TOTAL_NUM_LINES);
        }
        
        // filtro de número de segmentos por linha
        filterSegmentsByNumber(v_segments,properties.numChars);
        if(visitor != null) {
            visitor.visitValue(v_segments.size(), "Numero de linhas para processamento", VISIT_VERTICAL_FILTER_NUM_LINES);
        }
        
        // processamento de linhas
        for(int i = 0; i < v_segments.size(); i++) {
            if(visitor != null) {
                visitor.visitValue(i, "Processamento de linha iniciado", VISIT_LINE_PROCESS_INIT);
            }
            Vector<Plate> p = recognizePlateFromSegmentsLine(v_segments.elementAt(i),visitor);
            if(p != null)
                plates.addAll(p);
        }
        
        // verificação do resultado
        if(plates.size() > 0) {
            Plate[] ret = new Plate[plates.size()];
            plates.copyInto(ret);
            result.setCode(PlateRecognitionResult.PLATE_RECOGNIZED);
            result.setPlates(ret);
        } else {
            result.setCode(PlateRecognitionResult.PLATE_NOT_FOUND);
        }
    }
    
    /**
     * Faz o reconhecimento de placas um uma mesma linha de segmentos.
     * @param segments vetor de segmentos ordenados que representa a linha de segmentos
     * @param visitor visitante
     * @return vetor de objetos de placas
     */
    private Vector<Plate> recognizePlateFromSegmentsLine(Vector<Segment> segments, PlateRecognizerVisitor visitor) {
        Vector<Plate> ret = new Vector();
        int diff = segments.size() - properties.numChars + 1;
        
        if(visitor != null) {
            visitor.visitObject(segments.size(), "Numero de segmentos na linha", VISIT_LINE_SEGMENTS_COUNT);
        }
        
        if(diff < 1) {
            if(visitor != null) {
                visitor.visitLog("Numero insuficiente de sementos", VISIT_LINE_PROCESS_COMPLETE_INSUFFICIENT_SEGMENTS);
            }
            return null;
        }
        
        for(int i = 0; i < diff; i++) {
            Plate p = plateMatch(segments,i,visitor);
            if(p != null) {
                ret.add(p);
                if(visitor != null) {
                    visitor.visitObject(i, "Encontrado placa iniciado da posicao", VISIT_LINE_PLATE_FOUND);
                    visitor.visitImage(p.toImage(
                            properties.plateHorzMaxDiff - properties.plateCharWidthMax, 
                            (properties.blocks.elementAt(0).maxWidthDiffNextBlock+properties.blocks.elementAt(0).minWidthDiffNextBlock)/2), 
                            "Salvando placa", 0);
                }
            } else {
                if(visitor != null) {
                    visitor.visitObject(i, "Nao encontrado placa iniciado da posicao", VISIT_LINE_PLATE_NOT_FOUND);
                }
            }
        }
        
        if(visitor != null) {
            visitor.visitObject(ret.size(), "Processo de linha completo", VISIT_LINE_PROCESS_COMPLETE);
        }
        return ret;
    }
    
    /**
     * Verifica se uma placa pode ser encontrada a partir do segmento de entrada.<BR>
     * Esse método verifica se a partir de um determinado segmento em diante uma placa pode ser formada,
     * analisando as regras de formatação definida nas propriedades da placa. Ou seja, esse método
     * verifica a partir de um segmento definido, se as distâncias entre os próximos segmentos formam
     * os blocos da placa e se os segmentos estão dentro das regras de distâncias definidas.
     * Se uma placa for encontrada, ela é criada e retornada.
     * @param segments segmentos da linha
     * @param firstSegment primeiro segmento para teste
     * @param visitor visitante
     * @return placa se existir uma, null caso contrário
     */
    private Plate plateMatch(Vector<Segment> segments, int firstSegment, PlateRecognizerVisitor visitor) {
        double lastSegmentPosition = -1;
        BPlateRecognizerBlock lastBlock = null;
        int segment = firstSegment;
        
        // check
        for(int currentBlock = 0; currentBlock < properties.blocks.size(); currentBlock++) {
            BPlateRecognizerBlock block = properties.blocks.elementAt(currentBlock);
            for(int currentIndex = 0; currentIndex < block.numSegments; currentIndex++) {
                Segment seg = segments.elementAt(segment++);
                if(lastSegmentPosition == -1) {
                    lastSegmentPosition = seg.getRectangle().getCenterX();
                    continue;
                }
                double segPos = seg.getRectangle().getCenterX();
                double segDist = segPos - lastSegmentPosition;
                lastSegmentPosition = segPos;
                
                if(currentIndex == 0) {
                    if(segDist < lastBlock.minWidthDiffNextBlock || segDist > lastBlock.maxWidthDiffNextBlock) {
                        return null;
                    }
                } else {
                    if(segDist < properties.plateHorzMinDiff || segDist > properties.plateHorzMaxDiff) {
                        return null;
                    }
                }
            }
            lastBlock = block;
        }
        
        Plate p = new Plate();
        
        Vector<Vector<Segment>> plateSegments = new Vector();
        segment = firstSegment;
        
        for(int currentBlock = 0; currentBlock < properties.blocks.size(); currentBlock++) {
            BPlateRecognizerBlock block = properties.blocks.elementAt(currentBlock);
            Vector<Segment> blockSegments = new Vector();
            plateSegments.add(blockSegments);
            for(int currentIndex = 0; currentIndex < block.numSegments; currentIndex++) {
                Segment seg = segments.elementAt(segment++);
                blockSegments.add(seg);
            }
        }
        
        p.setBlocks(plateSegments);
        
        return p;
    }
    
    /**
     * Agrupa todos segmentos que estão em uma mesma linha.
     * @param segments segmentos
     * @param maxDiff diferença máxima em y dos segmentos
     * @return segmentos agrupados por linhas
     */
    private static Vector<Vector<Segment>> groupSegmentsByLine(Vector<Segment> segments, int maxDiff) {
        
        Vector<Segment> csegments = new Vector(segments);
        
        Vector<Vector<Segment>> ret = new Vector();
        
        while(!csegments.isEmpty()) {
            Segment seg = csegments.elementAt(0);
            csegments.removeElementAt(0);
            
            Vector<Segment> current = new Vector();
            current.add(seg);
            ret.add(current);
            
            int py = (int)seg.getRectangle().getY();
            int pey = py + (int)seg.getRectangle().getHeight();
            
            for(int i = 0; i < csegments.size(); i++) {
                
                Segment ts = csegments.elementAt(i);
                
                int tspy = (int)ts.getRectangle().getY();
                int tspey = tspy + (int)ts.getRectangle().getHeight();
                
                int diffPy = Math.abs(tspy - py);
                int diffPey = Math.abs(tspey - pey);
                
                if(diffPy <= maxDiff || diffPey <= maxDiff) {
                    current.add(ts);
                    csegments.removeElementAt(i--);
                }
            }
        }
        
        return ret;
    }
    
    /**
     * Filtra um vetor de vetores de segmentos, deixando apenas os vetores que contém no mínimo um número determinado
     * de segmentos.
     * @param segments vetor de vetores de segmentos
     * @param minNum número mínimo de segmentos desejados
     */
    private static void filterSegmentsByNumber(Vector<Vector<Segment>> segments, int minNum) {
        for(int i = 0; i < segments.size(); i++) {
            if(segments.elementAt(i).size() < minNum) {
                segments.removeElementAt(i--);
            }
        }
    }

}
